'abuphysicscomponent.monkey by Belimoth

Strict
Import abubasics		'for OppositeDirection()
Import abucomponent
Import abucollisionmap



Interface IPhysics
	Method Solid:Bool() Property
	Method Solid:Void(solid:Bool) Property
	Method CollisionGroup:Int() Property
	Method CollisionGroup:Void(group:Int) Property
	Method CollisionMask:Int() Property
	Method CollisionMask:Void(mask:Int) Property
	Method Move:Void(x:Int, y:Int)
	'Method HandleTraceResult:Void(result:TraceResult)
	Method IsTouching:Bool(direction:Int)
	Method OnCollision:Void(other:IEntity)
End



Class PhysicsComponent Extends EntityComponent Implements IPhysics

Private
	Field _link:list.Node<PhysicsComponent>
Public
	Global solver := New PhysicsSolver()
	
	Method Solid:Bool() Property Return _solid End
	Method Solid:Void(solid:Bool) Property _solid = solid End
	Method CollisionGroup:Int() Property Return _collisionGroup	End
	Method CollisionGroup:Void(group:Int) Property _collisionGroup = group End
	Method CollisionMask:Int() Property Return _collisionMask End
	Method CollisionMask:Void(mask:Int) Property _collisionMask = mask End
	
	Method New()
		Self.boundary = New AABB[4]
		Self.limit = New Int[4]
		Self._link = solver.Add(Self)
	End
	
	Method Finalize:Void()
		'_link.Remove()
		solver.participants.RemoveEach(Self)
	End
	
	Method Move:Void(x:Int, y:Int)			'just adds to displacement, Update() does the actual movement
		_dx += x
		_dy += y
	End
	
	Method IsTouching:Bool(direction:Int)		'NOTE only detects level geometry
		If (direction < 0) Or (direction > 3) Then Error "Invalid direction."			'TODO put this on all methods that take a direction parameter
		Local bound:AABB = boundary[direction]
		If bound = Null Then Return False
		
		Select direction
			Case DIRECTION_NORTH, DIRECTION_WEST
				If bound.GetEdge( OppositeDirection(direction) ) + 1 = GetEdge(direction) Then Return True
			Case DIRECTION_SOUTH, DIRECTION_EAST
				If bound.GetEdge( OppositeDirection(direction) ) - 1 = GetEdge(direction) Then Return True
			Default
		End
		Return False
	End
	
	Method OnCollision:Void(other:IEntity)		'pass up
		owner.OnCollision(other)
	End

Private
	Field _solid:Bool
	Field _collisionGroup:Int
	Field _collisionMask:Int

Public		'TODO this is not ideal
	Field _x:Int, _y:Int, _w:Int, _h:Int
	
Private
	Field _dx:Int, _dy:Int
	
	Field boundary:AABB[]
	Field limit:Int[]
	
	Method GetEdge:Int(direction:Int)
		Select direction
			Case DIRECTION_NORTH
				Return _y
			Case DIRECTION_EAST
				Return _x + _w - 1
			Case DIRECTION_SOUTH
				Return _y + _h - 1
			Case DIRECTION_WEST
				Return _x
			Default
		End
		Return 0
	End		'TODO have this class extend AABB instead of duplicating this
	
	Method PreSolve:Void()
		_x = owner.PositionComponent.X
		_y = owner.PositionComponent.Y
		_w = owner.SizeComponent.Width
		_h = owner.SizeComponent.Height
		_dx += owner.VelocityComponent.XDisplacement
		_dy += owner.VelocityComponent.YDisplacement
	End
	
	Method StepOnce:Bool()
		Local xPrevious:Int = _x
		Local yPrevious:Int = _y
		
		
		If Abs(_dx) > Abs(_dy)
			_StepX()
			_StepY()
		Else
			_StepY()
			_StepX()
		EndIf
		
		If (_x = xPrevious) And (_y = yPrevious) Then Return False		'TODO add   (...) Or ( (_dx = 0) And (_dy = 0) )   ?
		Return True
	End
	
	Method _StepX:Void()
		Local direction:Int
		Local sx:Int
		
		If _dx > 0
			direction = DIRECTION_EAST
			UpdateLimit(direction)
			sx = Min(_dx, limit[direction])
		Else
			direction = DIRECTION_WEST
			UpdateLimit(direction)
			sx = Max(_dx, limit[direction])
		EndIf
		
		_MoveX(sx)
	End
	
	Method _StepY:Void()
		Local direction:Int
		Local sy:Int
		
		If _dy > 0
			direction = DIRECTION_SOUTH
			UpdateLimit(direction)
			sy = Min(_dy, limit[direction])
		Else
			direction = DIRECTION_NORTH
			UpdateLimit(direction)
			sy = Max(_dy, limit[direction])
		EndIf
		
		_MoveY(sy)	
	End
	
	Method _MoveX:Void(amount:Int)
		_x += amount
		_dx -= amount
	End
	
	Method _MoveY:Void(amount:Int)
		_y += amount
		_dy -= amount
	End
	
	Method PostSolve:Void()
		owner.PositionComponent.X = _x
		owner.PositionComponent.Y = _y
		
		For Local direction:Int = Eachin DIRECTIONS_CARDINAL
			UpdateBoundary(direction)		'TODO optimize this
		Next
		If IsTouching(DIRECTION_NORTH) And owner.VelocityComponent.YVelocity < 0 Then owner.VelocityComponent.YVelocity = 0
		IF IsTouching(DIRECTION_SOUTH) And owner.VelocityComponent.YVelocity > 0 Then owner.VelocityComponent.YVelocity = 0
		If IsTouching(DIRECTION_WEST) And owner.VelocityComponent.XVelocity < 0 Then owner.VelocityComponent.XVelocity = 0
		If IsTouching(DIRECTION_EAST) And owner.VelocityComponent.XVelocity > 0 Then owner.VelocityComponent.XVelocity = 0
		
		_dx = 0
		_dy = 0
	End
	
	Method UpdateLimit:Void(direction:Int)
		UpdateBoundary(direction)
		
		If boundary[direction] = Null
			Select direction
				Case DIRECTION_NORTH, DIRECTION_WEST
					limit[direction] = -2147483648
				Case DIRECTION_SOUTH, DIRECTION_EAST
					limit[direction] = 2147483647
				Default
			End
			Return
		EndIf
		
		Select direction
			Case DIRECTION_NORTH
				limit[DIRECTION_NORTH] = boundary[DIRECTION_NORTH].GetEdge(DIRECTION_SOUTH) + 1 - Self.GetEdge(DIRECTION_NORTH)
			Case DIRECTION_SOUTH
				limit[DIRECTION_SOUTH] = boundary[DIRECTION_SOUTH].GetEdge(DIRECTION_NORTH) - 1 - Self.GetEdge(DIRECTION_SOUTH)
			Case DIRECTION_WEST
				limit[DIRECTION_WEST] = boundary[DIRECTION_WEST].GetEdge(DIRECTION_EAST) + 1 - Self.GetEdge(DIRECTION_WEST)
			Case DIRECTION_EAST
				limit[DIRECTION_EAST] = boundary[DIRECTION_EAST].GetEdge(DIRECTION_WEST) - 1 - Self.GetEdge(DIRECTION_EAST)
			Default
		End
	End
	
	Method UpdateBoundary:Void(direction:Int)
		boundary[direction] = PhysicsComponent.solver.Trace(Self, direction)
	End
End



Class PhysicsSolver				'TODO add this to documentation
	Field map:ICollisionMap
	Field participants := New List<PhysicsComponent>
	
	Method Add:list.Node<PhysicsComponent>(participant:PhysicsComponent)
		Return participants.AddLast(participant)
	End
	
	Method Solve:Void()
		Local participant:PhysicsComponent
		
		For participant = EachIn participants
			participant.PreSolve()
		Next
		
		While _Step()
		Wend
		
		For participant = EachIn participants
			participant.PostSolve()
		Next
		
		Local arr:PhysicsComponent[] = participants.ToArray()
		For Local a:Int = 0 To arr.Length - 2
			For Local b:Int = a + 1 To arr.Length - 1
				If arr[a]._solid And arr[b]._solid
					Local a_b:Int = arr[a]._collisionGroup & arr[b]._collisionMask
					Local b_a:Int = arr[b]._collisionGroup & arr[a]._collisionMask
					If a_b Or b_a
						If _RectsOverlap(arr[a]._x, arr[a]._y, arr[a]._w, arr[a]._h, arr[b]._x, arr[b]._y, arr[b]._w, arr[b]._h)
							If a_b Then arr[b].OnCollision(arr[a].owner)
							If b_a Then arr[a].OnCollision(arr[b].owner)
							'TODO this does not account for entities destroying themselves
						EndIf
					EndIf
				EndIf
			Next
		Next
	End
	
	Method Trace:AABB(aabb:PhysicsComponent, direction:Int)
		If map = Null Then Return Null
		Return map.Trace(aabb, direction)
	End
	
Private
	Method _Step:Bool()
		Local changed:Bool = False
		For Local entity := EachIn participants
			If entity.StepOnce() Then changed = True
		Next
		Local str:String = "False"
		If changed Then str = "True"
		'Print "changed: " + str
		Return changed
	End
	
	Method _RectsOverlap:Bool(x0:Int, y0:Int, w0:Int, h0:Int, x1:Int, y1:Int, w1:Int, h1:Int)
		Local xx0:Int = Max(x0, x1)
		Local yy0:Int = Max(y0, y1)
		Local xx1:Int = Min(x0 + w0 - 1, x1 + w1 - 1)
		Local yy1:Int = Min(y0 + h0 - 1, y1 + h1 - 1)
		If xx1 - xx0 > 0 And yy1 - yy0 > 0 Then Return True
		Return False
	End
End


Private

Global _group:Int = 1



Public

Function AllocateCollisionGroup:Int()
	Local group:Int = _group
	_group *= 2
	If (_group = 0) And (group = 0) Then Error "Maximum number of collision groups exceeded."
	Return group
End



Function UpdatePhysics:Void()
	PhysicsComponent.solver.Solve()
End